/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.loaders; import java.lang.ref.WeakReference; import java.io.*; import java.util.*; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; import org.openide.*; import org.openide.filesystems.*; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.enum.SequenceEnumeration; import org.openide.util.enum.SingletonEnumeration; import org.openide.util.WeakListener; import org.openide.util.Mutex; import org.openide.nodes.Node; import org.openide.nodes.CookieSet; /** Provides support for handling of data objects with multiple files. * One file is represented by one {@link Entry}. Each handler * has one {@link #getPrimaryEntry primary} entry and zero or more secondary entries. * * @author Ales Novak, Jaroslav Tulach, Ian Formanek */ public class MultiDataObject extends DataObject { /** generated Serialized Version UID */ static final long serialVersionUID = -7750146802134210308L; /** getPrimaryEntry() is intended to have all inetligence for copy/move/... */ private Entry primary; /** Map of secondary entries and its files. (FileObject, Entry) * @associates Entry*/ private HashMap secondary = new HashMap (11); /** array of cookies for this object */ private CookieSet cookieSet; /** listener for changes in the cookie set */ private EntryL cookieL = new EntryL (); /** listener to attach to file entries to listen if they are not removed */ private FileChangeListener entryL = WeakListener.fileChange (cookieL, null); /** Create a handler. * @param fo the primary file object * @param loader loader of this data object */ public MultiDataObject(FileObject fo, MultiFileLoader loader) throws DataObjectExistsException { super(fo, loader); } /** Getter for the multi file loader that created this * object. * * @return the multi loader for the object */ public final MultiFileLoader getMultiFileLoader () { return (MultiFileLoader)getLoader (); } /* Method to access all FileObjects used by this DataObject. * These file objects should have set the important flag to * allow the requester to distingush between important and * unimportant files. * * @return set of FileObjects */ public Set files () { // enumeration of all files getMultiFileLoader ().checkFiles (this); HashSet s; synchronized (secondary) { s = new HashSet (secondary.keySet ()); s.add (getPrimaryFile ()); } return s; } /* Getter for delete action. * @return true if the object can be deleted */ public boolean isDeleteAllowed() { return !getPrimaryFile ().isReadOnly (); } /* Getter for copy action. * @return true if the object can be copied */ public boolean isCopyAllowed() { return true; } /* Getter for move action. * @return true if the object can be moved */ public boolean isMoveAllowed() { return !getPrimaryFile ().isReadOnly (); } /* Getter for rename action. * @return true if the object can be renamed */ public boolean isRenameAllowed () { return !getPrimaryFile ().isReadOnly (); } /* Help context for this object. * @return help context */ public HelpCtx getHelpCtx() { return HelpCtx.DEFAULT_HELP; } /** Provides node that should represent this data object. * * @return the node representation * @see DataNode */ protected Node createNodeDelegate () { DataNode dataNode = (DataNode) super.createNodeDelegate (); return dataNode; } /** Add a new secondary entry to the list. * @param fe the entry to add */ protected final void addSecondaryEntry (Entry fe) { synchronized (secondary) { secondary.put (fe.getFile (), fe); fe.getFile ().addFileChangeListener (entryL); } firePropertyChangeLater (PROP_FILES, null, null); } /** Remove a secondary entry from the list. * @param fe the entry to remove */ protected final void removeSecondaryEntry (Entry fe) { synchronized (secondary) { secondary.remove (fe.getFile ()); } firePropertyChangeLater (PROP_FILES, null, null); } /** All secondary entries are recognized. Called from multi file object. * @param recognized object to mark recognized file to */ final void markSecondaryEntriesRecognized (DataLoader.RecognizedFiles recognized) { synchronized (secondary) { Iterator it = secondary.keySet ().iterator (); while (it.hasNext ()) { FileObject fo=(FileObject)it.next (); recognized.markRecognized (fo); } } } /** Tests whether this file is between entries and if not, * creates a secondary entry for it and adds it into set of * secondary entries. * <P> * This method should be used in constructor of MultiDataObject to * register all the important files, that could belong to this data object. * As example, our XMLDataObject, tries to locate its <CODE>xmlinfo</CODE> * file and then do register it * * @param fo the file to register (can be null, then the action is ignored) * @return the entry associated to this file object (returns primary entry if the fo is null) */ protected final Entry registerEntry (FileObject fo) { synchronized (secondary) { if (fo == null) { // is it ok, to do this or somebody would like to see different behavour? return primary; } if (fo.equals (getPrimaryFile ())) { return primary; } Entry e = (Entry)secondary.get (fo); if (e != null) { return e; } // add it into set of entries e = getMultiFileLoader ().createSecondaryEntry (this, fo); addSecondaryEntry (e); return e; } } /** Removes the entry from the set of secondary entries. * Called from the EntryL listener. */ final void removeFile (FileObject fo) { synchronized (secondary) { Entry e = (Entry)secondary.get (fo); if (e != null) { removeSecondaryEntry (e); } } } /** Get the primary entry. * @return the entry */ public final Entry getPrimaryEntry () { synchronized (secondary) { if (primary == null) { primary = getMultiFileLoader ().createPrimaryEntry (this, getPrimaryFile ()); } return primary; } } /** Get secondary entries. * @return immutable set of {@link Entry}s */ public final Set secondaryEntries () { synchronized (this) { return new HashSet (secondary.values ()); } } /** For a given file, find the associated secondary entry. * @param fo file object * @return the entry associated with the file object, or <code>null</code> if there is no * such entry */ public final Entry findSecondaryEntry (FileObject fo) { return (Entry)secondary.get (fo); } //methods overriding DataObjectHandler's abstract methods /* Obtains lock for primary file by asking getPrimaryEntry() entry. * * @return the lock for primary file * @exception IOException if it is not possible to set the template * state. */ protected FileLock takePrimaryFileLock () throws IOException { return getPrimaryEntry ().takeLock (); } // XXX does nothing of the sort --jglick /** Check if in specific folder exists fileobject with the same name. * If it exists user is asked for confirmation to rewrite, rename or cancel operation. * @param folder destination folder * @return the suffix which should be added to the name or null if operation is cancelled */ private static String existInFolder(FileObject fo, FileObject folder) { String orig = fo.getName (); String name = FileUtil.findFreeFileName( folder, orig, fo.getExt () ); if (name.length () <= orig.length ()) { return ""; // NOI18N } else { return name.substring (orig.length ()); } } /** Copies primary and secondary files to new folder. * May ask for user confirmation before overwriting. * @param df the new folder * @return data object for the new primary * @throws IOException if there was a problem copying * @throws UserCancelException if the user cancelled the copy */ protected synchronized DataObject handleCopy (DataFolder df) throws IOException { FileObject fo; synchronized (secondary) { String suffix = existInFolder( getPrimaryEntry().getFile(), df.getPrimaryFile () ); if (suffix == null) throw new org.openide.util.UserCancelException(); fo = getPrimaryEntry ().copy (df.getPrimaryFile (), suffix); Iterator it = secondary.values ().iterator (); while (it.hasNext ()) { ((Entry)it.next()).copy (df.getPrimaryFile (), suffix); } } try { return getMultiFileLoader ().createMultiObject (fo); } catch (DataObjectExistsException ex) { return ex.getDataObject (); } } /* Deletes all secondary entries, removes them from the set of * secondary entries and then deletes the getPrimaryEntry() entry. */ protected void handleDelete() throws IOException { synchronized (secondary) { Iterator it = secondary.entrySet ().iterator (); while (it.hasNext ()) { Map.Entry e = (Map.Entry)it.next (); ((Entry)e.getValue ()).delete (); it.remove (); } getPrimaryEntry().delete(); } } /* Renames all entries and changes their files to new ones. */ protected FileObject handleRename (String name) throws IOException { synchronized (secondary) { getPrimaryEntry ().file = getPrimaryEntry().rename (name); HashMap add = null; Iterator it = secondary.entrySet ().iterator (); while (it.hasNext ()) { Map.Entry e = (Map.Entry)it.next (); FileObject fo = ((Entry)e.getValue ()).rename (name); if (fo == null) { // remove the entry it.remove (); } else { if (!fo.equals (e.getKey ())) { // put the new one into change table if (add == null) add = new HashMap (); Entry entry = (Entry)e.getValue (); add.put (fo, entry); entry.file = fo; // changed the file => remove the file it.remove (); } } } // if there has been a change in files, apply it if (add != null) { secondary.putAll (add); firePropertyChangeLater (PROP_FILES, null, null); } return getPrimaryEntry ().file; } } /** Moves primary and secondary files to a new folder. * May ask for user confirmation before overwriting. * @param df the new folder * @return the moved primary file object * @throws IOException if there was a problem moving * @throws UserCancelException if the user cancelled the move */ protected FileObject handleMove (DataFolder df) throws IOException { synchronized (secondary) { String suffix = existInFolder(getPrimaryEntry().getFile(), df.getPrimaryFile ()); if (suffix == null) throw new org.openide.util.UserCancelException(); getPrimaryEntry ().file = getPrimaryEntry ().move (df.getPrimaryFile (), suffix); HashMap add = null; Iterator it = secondary.entrySet ().iterator (); while (it.hasNext ()) { Map.Entry e = (Map.Entry)it.next (); FileObject fo = ((Entry)e.getValue ()).move (df.getPrimaryFile (), suffix); if (fo == null) { // remove the entry it.remove (); } else { if (!fo.equals (e.getKey ())) { // put the new one into change table if (add == null) add = new HashMap (); Entry entry = (Entry)e.getValue (); add.put (fo, entry); entry.file = fo; // changed the file => remove the file it.remove (); } } } // if there has been a change in files, apply it if (add != null) { secondary.putAll (add); firePropertyChangeLater (PROP_FILES, null, null); } return getPrimaryEntry ().file; } } /* Creates new object from template. * @exception IOException */ protected DataObject handleCreateFromTemplate ( DataFolder df, String name ) throws IOException { FileObject fo; synchronized (secondary) { if (name == null) { name = FileUtil.findFreeFileName( df.getPrimaryFile (), getPrimaryFile ().getName (), getPrimaryFile ().getExt () ); } fo = getPrimaryEntry().createFromTemplate (df.getPrimaryFile (), name); Iterator it = secondary.values ().iterator (); while (it.hasNext ()) { ((Entry)it.next()).createFromTemplate (df.getPrimaryFile (), name); } } try { return getMultiFileLoader ().createMultiObject (fo); } catch (DataObjectExistsException ex) { return ex.getDataObject (); } } // XXX: // Protected to be accessible // only from subclasses. /** Set the set of cookies. * To the provided cookie set a listener is attached, * and any change to the set is propagated by * firing a change on {@link #PROP_COOKIE}. * * @param s the cookie set to use */ public synchronized void setCookieSet (CookieSet s) { if (cookieSet != null) { cookieSet.removeChangeListener (cookieL); } s.addChangeListener (cookieL); cookieSet = s; fireCookieChange (); } /** Get the set of cookies. * If the set had been * previously set by {@link #setCookieSet}, that set * is returned. Otherwise an empty set is * returned. * * @return the cookie set (never <code>null</code>) */ public CookieSet getCookieSet () { CookieSet s = cookieSet; if (s != null) return s; synchronized (this) { if (cookieSet != null) return cookieSet; // sets empty sheet and adds a listener to it setCookieSet (new CookieSet ()); return cookieSet; } } /** Look for a cookie in the current cookie set matching the requested class. * * @param type the class to look for * @return an instance of that class, or <code>null</code> if this class of cookie * is not supported */ public Node.Cookie getCookie (Class type) { CookieSet c = cookieSet; if (c != null) { Node.Cookie cookie = c.getCookie (type); if (cookie != null) return cookie; } return super.getCookie (type); } /** Fires cookie change. */ final void fireCookieChange () { firePropertyChange (PROP_COOKIE, null, null); } /** Fires property change but in event thread. */ private void firePropertyChangeLater ( final String name, final Object oldV, final Object newV ) { Mutex.EVENT.readAccess (new Runnable () { public void run () { firePropertyChange (name, oldV, newV); } }); } /** Represents one file in a {@link MultiDataObject group data object}. */ public abstract class Entry implements java.io.Serializable { /** generated Serialized Version UID */ static final long serialVersionUID = 6024795908818133571L; /** modified from MultiDataObject operations, that is why it is package * private */ FileObject file; /** This factory is used for creating new clones of the holding lock for internal * use of this DataObject. It factory is null it means that the file entry is not */ private transient WeakReference lock; protected Entry (FileObject file) { this.file = file; } /** Get the file this entry works with. */ public final FileObject getFile () { return file; } /** Get the multi data object this entry is assigned to. * @return the data object */ public final MultiDataObject getDataObject () { return MultiDataObject.this; } /** Called when the entry is to be copied. * Depending on the entry type, it should either copy the underlying <code>FileObject</code>, * or do nothing (if it cannot be copied). * @param f the folder to create this entry in * @param name the new name to use * @return the copied <code>FileObject</code> or <code>null</code> if it cannot be copied * @exception IOException when the operation fails */ public abstract FileObject copy (FileObject f, String suffix) throws IOException; /** Called when the entry is to be renamed. * Depending on the entry type, it should either rename the underlying <code>FileObject</code>, * or delete it (if it cannot be renamed). * @param name the new name * @return the renamed <code>FileObject</code> or <code>null</code> if it has been deleted * @exception IOException when the operation fails */ public abstract FileObject rename (String name) throws IOException; /** Called when the entry is to be moved. * Depending on the entry type, it should either move the underlying <code>FileObject</code>, * or delete it (if it cannot be moved). * @param f the folder to move this entry to * @param suffix the suffix to use * @return the moved <code>FileObject</code> or <code>null</code> if it has been deleted * @exception IOException when the operation fails */ public abstract FileObject move (FileObject f, String suffix) throws IOException; /** Called when the entry is to be deleted. * @exception IOException when the operation fails */ public abstract void delete () throws IOException; /** Called when the entry is to be created from a template. * Depending on the entry type, it should either copy the underlying <code>FileObject</code>, * or do nothing (if it cannot be copied). * @param f the folder to create this entry in * @param name the new name to use * @return the copied <code>FileObject</code> or <code>null</code> if it cannot be copied * @exception IOException when the operation fails */ public abstract FileObject createFromTemplate (FileObject f, String name) throws IOException; /** Try to lock this file entry. * @return the lock if the operation was successful; otherwise <code>null</code> * @throws IOException if the lock could not be taken */ public FileLock takeLock() throws IOException { FileLock l = lock == null ? null : (FileLock)lock.get (); if (l == null || !l.isValid ()){ l = getFile ().lock (); lock = new WeakReference (l); } return l; } /** Tests whether the entry is locked. * @return <code>true</code> if so */ public boolean isLocked() { FileLock l = lock == null ? null : (FileLock)lock.get (); return l != null && l.isValid (); } public boolean equals(Object o) { if (! (o instanceof Entry)) return false; return file.equals(((Entry) o).file); } public int hashCode() { return file.hashCode(); } /** Make a Serialization replacement. * The entry is identified by the * file object is holds. When serialized, it stores the * file object and the data object. On deserialization * it finds the data object and creates the right entry * for it. */ protected Object writeReplace () { return new EntryReplace (file); } } /** File change listener attached to entries that * removes the entry from the hash map if it is deleted. */ private class EntryL extends FileChangeAdapter implements ChangeListener { /** Fired when a file has been deleted. * @param fe the event describing context where action has taken place */ public void fileDeleted (FileEvent fe) { removeFile (fe.getFile ()); } /** State changed */ public void stateChanged (ChangeEvent ev) { fireCookieChange (); } } /** Entry replace. */ private static final class EntryReplace extends Object implements java.io.Serializable { /** generated Serialized Version UID */ static final long serialVersionUID = -1498798537289529182L; /** file object of the entry */ private FileObject file; /** entry to be used during read */ private transient Entry entry; public EntryReplace (FileObject fo) { file = fo; } private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject (); try { DataObject obj = DataObject.find (file); if (obj instanceof MultiDataObject) { MultiDataObject m = (MultiDataObject)obj; if (file.equals (m.getPrimaryFile ())) { // primary entry entry = m.getPrimaryEntry (); } else { // secondary entry Entry e = (Entry)m.findSecondaryEntry (file); if (e == null) { throw new InvalidObjectException (obj.toString ()); } // remember the entry entry = e; } } } catch (DataObjectNotFoundException ex) { throw new InvalidObjectException (ex.getMessage ()); } } public Object readResolve () { return entry; } } } /* * Log * 31 Gandalf 1.30 1/12/00 Ian Formanek NOI18N * 30 Gandalf 1.29 12/2/99 Jaroslav Tulach DataObject.files () * should return correct results for all MultiFileObject subclasses that * collects objects from one folder. * 29 Gandalf 1.28 11/5/99 Jaroslav Tulach WeakListener has now * registration methods. * 28 Gandalf 1.27 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 27 Gandalf 1.26 9/6/99 Jaroslav Tulach * 26 Gandalf 1.25 9/3/99 Jaroslav Tulach Different synch. * 25 Gandalf 1.24 7/21/99 Jaroslav Tulach MultiDataObject can mark * easily mark secondary entries in constructor as belonging to the * object. * 24 Gandalf 1.23 7/15/99 Ian Formanek Fixed bug 1903 - * Exception during renaming bookmark, which was added to package. * 23 Gandalf 1.22 6/24/99 Jaroslav Tulach Property * synchronization. * 22 Gandalf 1.21 6/8/99 Ian Formanek ---- Package Change To * org.openide ---- * 21 Gandalf 1.20 5/17/99 Jaroslav Tulach Fix 1703 * 20 Gandalf 1.19 5/7/99 Michal Fadljevic * 19 Gandalf 1.18 5/7/99 Michal Fadljevic * 18 Gandalf 1.17 5/7/99 Michal Fadljevic registerFile() first * condition clarified * 17 Gandalf 1.16 4/27/99 Jesse Glick new HelpCtx () -> * HelpCtx.DEFAULT_HELP. * 16 Gandalf 1.15 4/22/99 Jaroslav Tulach Does not garbage listner * EntryL * 15 Gandalf 1.14 4/21/99 Jaroslav Tulach * 14 Gandalf 1.13 3/17/99 Jaroslav Tulach * 13 Gandalf 1.12 3/15/99 Jesse Glick [JavaDoc] * 12 Gandalf 1.11 3/14/99 Jaroslav Tulach Change of * MultiDataObject.Entry. * 11 Gandalf 1.10 3/14/99 Jaroslav Tulach * 10 Gandalf 1.9 3/10/99 Jesse Glick [JavaDoc] * 9 Gandalf 1.8 3/9/99 Jaroslav Tulach Works even there is no * secondary entry. * 8 Gandalf 1.7 3/3/99 David Simonek * 7 Gandalf 1.6 2/11/99 Jan Jancura Support for icons. * 6 Gandalf 1.5 2/1/99 Jaroslav Tulach * 5 Gandalf 1.4 2/1/99 Jaroslav Tulach Entry is replaceable. * 4 Gandalf 1.3 1/6/99 Ian Formanek Property update. * 3 Gandalf 1.2 1/6/99 Ales Novak * 2 Gandalf 1.1 1/6/99 Ian Formanek * 1 Gandalf 1.0 1/5/99 Ian Formanek * $ * Beta Change History: * 0 Tuborg 0.13 --/--/98 Jan Formanek changed FileEntry.createFromTemplate - added <String name>parameter * 0 Tuborg 0.13 --/--/98 Jan Formanek added implementation in PrimaryEntrySupport.createFromTemplate as copy + rename * 0 Tuborg 0.13 --/--/98 Jan Formanek added implementation in MirroringEntry.createFromTemplate as copy + rename * 0 Tuborg 0.14 --/--/98 Ales Novak overwritten to DataObject * 0 Tuborg 0.17 --/--/98 Jaroslav Tulach PrimaryEntrySupport has method createDataObject to allow subclasses to do more * 0 Tuborg 0.17 --/--/98 Jaroslav Tulach clever creation of data objects * 0 Tuborg 0.18 --/--/98 Jan Formanek changes in FileEntry - copy, createFromTemplate now return FileObject * 0 Tuborg 0.18 --/--/98 Jan Formanek added method createDataObject * 0 Tuborg 0.19 --/--/98 Ales Novak NotImplementedException removed from numb entry - template * 0 Tuborg 0.20 --/--/98 Petr Hamernik locking redesigned * 0 Tuborg 0.21 --/--/98 Petr Hamernik Entries little redesigned * 0 Tuborg 0.22 --/--/98 Petr Hamernik some improvements */